package com.lam.code.configuration;

import com.lam.code.configuration.prop.JWTProperties;
import com.lam.code.configuration.prop.ShiroProperties;
import com.lam.code.configuration.shiro.DBRealm;
import com.lam.code.configuration.shiro.JWTFilter;
import com.lam.code.configuration.shiro.JWTRealm;
import com.lam.code.configuration.shiro.MultiRealmAuthenticator;
import com.lam.code.service.UserService;
import org.apache.shiro.authc.Authenticator;
import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSessionStorageEvaluator;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.DelegatingFilterProxy;

import javax.annotation.Resource;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import java.util.Arrays;
import java.util.Map;

/**
 * @author lam
 * @description shiro配置类
 *
 * 参考以下几个开源项目
 * springboot-demo  https://github.com/chilexun/springboot-demo
 * story-admin
 * jeect-boot
 * springboot-plus
 *
 * @date 2019/12/22 23:07
 */
@Configuration
@EnableConfigurationProperties({
        ShiroProperties.class
})
public class ShiroConfiguration {

    @Resource
    private ShiroProperties shiroProperties;

    /**
     * 用于JWT token认证的realm
     */
    @Bean
    public Realm jwtRealm(UserService userService) {
        return new JWTRealm(userService);
    }

    /**
     * 用于用户名密码登录时认证的realm
     */
    @Bean
    public Realm dbRealm(UserService userService) {
        return new DBRealm(userService, shiroProperties);
    }

    /**
     * 覆盖ShiroAnnotationProcessorAutoConfiguration中的defaultAdvisorAutoProxyCreator()，
     * 主要原因是我们要强制使用cglib,防止重复代理和可能引起代理出错的问题
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib，防止重复代理和可能引起代理出错的问题 https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 初始化Authenticator
     */
    @Bean
    public Authenticator authenticator(UserService userService) {
//        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
        ModularRealmAuthenticator authenticator = new MultiRealmAuthenticator();
        //设置两个Realm，一个用于用户登录验证和访问权限获取；一个用于jwt token的认证
        authenticator.setRealms(Arrays.asList(jwtRealm(userService), dbRealm(userService)));
        //设置多个realm认证策略，一个成功即跳过其它的
        authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
        return authenticator;
    }

    /**
     * 覆盖ShiroAutoConfiguration中的sessionStorageEvaluator()，主要原因是使用Jwt验证，无需开启session
     * <p>
     * 1.禁用session, 不保存用户登录状态。保证每次请求都重新认证。
     * 需要注意的是，如果用户代码里调用Subject.getSession()还是可以用session，
     * 如果要完全禁用，要配合下面的noSessionCreation的Filter来实现
     * <p>
     * 2.无需配置cacheManger,因为cacheManager主要是对session(SessionDAO)增，删，改，查操作,还有用户缓存及用户权限缓存，我们无需配置redis来保存这个。
     * 因为我们没有session,只需要在redis缓存用户的权限信息即可。
     *
     * 当然也可以配置shiro-redis第三方插件，实现对用户及用户权限缓存，不对session进行处理（不配置RedisSessionDao)
     *
     * <p>
     * 3.因为springboot自动配置securityManager，所以我们可以不声明这个bean。除非想要增加cacheManger，或做一些特殊的处理。
     */
    @Bean
    protected SessionStorageEvaluator sessionStorageEvaluator() {
        DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        return sessionStorageEvaluator;
    }

    /**
     * 设置过滤器，将自定义的Filter加入
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,
                                              JWTProperties jwtProperties,
                                              UserService userService) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        shiroFilter.setLoginUrl(shiroProperties.getLoginUrl());

        // 添加jwt过滤器
        Map<String, Filter> filterMap = shiroFilter.getFilters();
        filterMap.put("jwt", new JWTFilter(userService, jwtProperties));
        shiroFilter.setFilters(filterMap);

        //动态配置拦截器注入
        Map<String, String> filterChainDefinitionMap = shiroFilter.getFilterChainDefinitionMap();
        // 设置免认证 url
        String[] anonUrls = StringUtils.split(shiroProperties.getAnonUrl(), ",");
        if (anonUrls != null) {
            for (String url : anonUrls) {
                //filterChainDefinitionMap.put(url, "anon");
                filterChainDefinitionMap.put(url, "noSessionCreation,jwt[permissive]");
            }
        }

        //login不做认证，noSessionCreation的作用是用户在操作session时会抛异常
        //filterChainDefinitionMap.put("/login", "noSessionCreation,anon");
        filterChainDefinitionMap.put("/login", "noSessionCreation,jwt[permissive]");
        //做用户认证，permissive参数的作用是当token无效时也允许请求访问，不会返回鉴权未通过的错误
        filterChainDefinitionMap.put("/logout", "noSessionCreation,jwt[permissive]");
        // 默认进行用户鉴权,过滤链定义，从上向下顺序执行，一般将/**放在最为下边
        filterChainDefinitionMap.put("/**", "noSessionCreation,jwt");
        shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilter;
    }

    /**
     * 注册shiro的Filter，拦截请求
     */
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<>();
        DelegatingFilterProxy proxy = new DelegatingFilterProxy();
        proxy.setTargetFilterLifecycle(true);
        proxy.setTargetBeanName("shiroFilter");
        filterRegistration.setFilter(proxy);
        filterRegistration.setAsyncSupported(true);
        filterRegistration.setEnabled(true);
        filterRegistration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
        return filterRegistration;
    }

}
